//---------------------------------------------------------------------
//  This file is part of the Background Motion solution.
// 
//  Copyright (C) Mindscape (TM).  All rights reserved.
//  http://www.mindscape.co.nz
// 
//  THIS CODE AND INFORMATION ARE PROVIDED AS IS WITHOUT WARRANTY OF ANY
//  KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
//  IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
//  PARTICULAR PURPOSE.
//---------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Security.Permissions;
using System.Text;
using System.Xml;

using Mindscape.BackgroundMotion.Core.Model;
using Mindscape.BackgroundMotion.Core.Providers;
using Mindscape.BackgroundMotion.Core.Search;
using Mindscape.BackgroundMotion.Core.Utilities;
using Mindscape.BackgroundMotion.Model;
using Mindscape.BackgroundMotion.Website.Infrastructure;

using RssToolkit;
using System.Linq;

namespace Mindscape.BackgroundMotion.Website.Controllers
{
  public sealed class ContributionController : ControllerBase, IContributionController
  {
    /// <summary>
    /// View a <see cref="Contribution"/>
    /// </summary>
    public Contribution ViewContribution(int id)
    {
      return GetContribution(id, true);
    }

    /// <summary>
    /// Gets a <see cref="Contribution"/>.
    /// </summary>
    /// <param name="id">The id of the <see cref="Contribution"/> to return.</param>
    public Contribution GetContribution(int id)
    {
      return GetContribution(id, false);
    }

    /// <summary>
    /// Gets a Contribution from an external provider which can translate the requested identifier
    /// </summary>
    /// <param name="provider">The provider.</param>
    /// <param name="identifier">The identifier which will be understood by the provider and return a Contribution.</param>
    public Contribution GetContributionFromExternalProvider(string provider, string identifier)
    {
      IResourceProvider resourceProvider = ResourceProviderLocator.Locate(provider);
      if (resourceProvider == null) return null;
      
      ResourceDetails details = resourceProvider.GetDetails(identifier);

      if (details == null) return null;

      Contribution contribution = new Contribution();
      contribution.ContentTypeId = details.ContentType;

      if (details.Title != null && details.Title.Length != 0)
        contribution.Title = details.Title;
      if (details.Description != null && details.Description.Length != 0)
      {
        // A common situation is for the description to be too long and is not immediately
        // obvious to the user that this is the case if padded with whitespace at the start
        if (details.Description.Length > 255)
        {
          details.Description = details.Description.Substring(0, 255);
        }

        contribution.Description = details.Description;
      }
      if (details.Url != null && details.Url.Length != 0)
        contribution.PreviewLink = details.Url;
      if (details.ThumbnailUrl != null && details.ThumbnailUrl.Length != 0)
        contribution.PreviewImage = details.ThumbnailUrl;
      if (details.DownloadUrl != null && details.DownloadUrl.Length != 0)
        contribution.DownloadLink = details.DownloadUrl;

      if (((Decimal?)details.GeoX).HasValue)
      {
        contribution.GeoX = (Decimal?)details.GeoX;
      }

      if (((Decimal?)details.GeoY).HasValue)
      {
        contribution.GeoY = (Decimal?) details.GeoY;
      }

      if (details.Tags != null)
      {
        StringBuilder tagsStringBuilder = new StringBuilder();

        foreach (string tag in details.Tags)
        {
          if (tag.IndexOf(' ') > 0)
          {
            tagsStringBuilder.AppendFormat("\"{0}\" ", tag);
          }
          else
          {
            tagsStringBuilder.Append(tag);
            tagsStringBuilder.Append(" ");
          }
        }

        contribution.Tags = tagsStringBuilder.ToString();
      }

      return contribution;
    }

    /// <summary>
    /// Gets the unmoderated contributions.
    /// </summary>
    /// <remarks>Security is being applied to prevent unauthenticated users making this call</remarks>
    [PrincipalPermission(SecurityAction.Demand, Authenticated=true)]
    public IList<Contribution> GetUnmoderatedContributions()
    {
      using (UnitOfWork.Begin())
      {
        return UnmoderatedContributions.ToList();
      }
    }

    /// <summary>
    /// Gets the most recent list of <see cref="Contribution"/>s filtered by <see cref="ContentType"/>
    /// </summary>
    /// <param name="contentTypeId">The identifier of the ContentType to filter by.</param>
    public IList<Contribution> GetMostRecentByContentType(int contentTypeId)
    {
      return GetMostRecentByContentType(contentTypeId, 0);
    }

    /// <summary>
    /// Gets the most recent list of <see cref="Contribution"/>s filtered by <see cref="ContentType"/>
    /// </summary>
    /// <param name="contentTypeId">The identifier of the ContentType to filter by.</param>
    /// <param name="skip">The amount of records to skip, used by the paging infrastructure</param>
    public IList<Contribution> GetMostRecentByContentType(int contentTypeId, int skip)
    {
      using (UnitOfWork.Begin())
      {
        return ModeratedContributions
          .Where(c => c.ContentTypeId == contentTypeId)
          .OrderByDescending(c => c.AddedOn)
          .Skip(skip)
          .Take(Constants.PageSize).ToList();
      }
    }

    /// <summary>
    /// Returns the number of most recent entries for a given content type, this is used by the Search() method
    /// </summary>
    private int GetNumberOfMostRecentByContentType(int contentTypeId)
    {
      return ModeratedContributions
         .Where(c => c.ContentTypeId == contentTypeId)
         .Count();
    }

    /// <summary>
    /// Searches the specified page number.
    /// </summary>
    /// <param name="pageNumber">The page number.</param>
    public ISearchResult Search(int pageNumber)
    {
      if (pageNumber <= 0 )
      {
        pageNumber = 1;
      }
      
      SearchResult result = new SearchResult();
      result.PageNumber = pageNumber;
      result.PageSize = Constants.PageSize;
      int skip = (pageNumber - 1)*Constants.PageSize;
      string query = GetParameter<string>(Constants.Parameters.Query);

      if (!string.IsNullOrEmpty(query))
      {
        int totalResults = 0; 
        result.Contributions = SearchEngine<Contribution>.Search(query, Constants.PageSize, pageNumber, out totalResults);
        result.TotalResults = totalResults;
      }

      using (UnitOfWork.Begin())
      {
        int? tag = GetParameter<int?>(Constants.Parameters.Tag);

        if (tag != null)
        {
           result.Contributions = Repository<ContributionTag>
             .Find(ct => ct.Tag.Id == tag)
             .Where(ct => ct.Contribution.ApprovedById != null)
             .Select(ct => ct.Contribution)
             .Take(Constants.PageSize)
             .ToList();

          result.TotalResults = Repository<ContributionTag>
             .Find(ct => ct.Tag.Id == tag)
             .Where(ct => ct.Contribution.ApprovedById != null)
             .Count();
        }

        string filter = GetParameter<string>(Constants.Parameters.Filter);

        if (!string.IsNullOrEmpty(filter))
        {
          result.TotalResults = ModeratedContributions.OrderByDescending(c => c.AddedOn).Count();

          if (Constants.Filters.MostRecent.Equals(filter, StringComparison.InvariantCultureIgnoreCase))
          {
            result.Contributions = ModeratedContributions
              .OrderByDescending(c => c.AddedOn)
              .Skip(skip)
              .Take(Constants.PageSize)
              .ToList();
          }
          if (Constants.Filters.MostViewed.Equals(filter, StringComparison.InvariantCultureIgnoreCase))
          {
            result.Contributions = ModeratedContributions
              .OrderByDescending(c => c.Views)
              .Skip(skip)
              .Take(Constants.PageSize).ToList();
          }
          if (Constants.Filters.TopRated.Equals(filter, StringComparison.InvariantCultureIgnoreCase))
          {
            result.Contributions = ModeratedContributions
              .OrderByDescending(c => c.RatingTotal/(c.RatingCount+1))
              .OrderByDescending(c => c.RatingTotal)
              .Skip(skip)
              .Take(Constants.PageSize)
              .ToList();
          }
          if (Constants.Filters.Videos.Equals(filter, StringComparison.InvariantCultureIgnoreCase))
          {
            result.Contributions = GetMostRecentByContentType(Constants.ContentTypeIds.Video, skip);
            result.TotalResults = GetNumberOfMostRecentByContentType(Constants.ContentTypeIds.Video);
          }
          if (Constants.Filters.Images.Equals(filter, StringComparison.InvariantCultureIgnoreCase))
          {
            result.Contributions = GetMostRecentByContentType(Constants.ContentTypeIds.Image, skip);
            result.TotalResults = GetNumberOfMostRecentByContentType(Constants.ContentTypeIds.Image);
          }
          if (Constants.Filters.Today.Equals(filter, StringComparison.InvariantCultureIgnoreCase))
          {
            result.Contributions = GetWithinDays(-1, skip);
            result.TotalResults = GetNumberWithinDays(-1);
          }
          if (Constants.Filters.ThisWeek.Equals(filter, StringComparison.InvariantCultureIgnoreCase))
          {
            result.Contributions = GetWithinDays(-7,  skip);
            result.TotalResults = GetNumberWithinDays(-7);
          }
          if (Constants.Filters.ThisMonth.Equals(filter, StringComparison.InvariantCultureIgnoreCase))
          {
            result.Contributions = GetWithinDays(-30, skip);
            result.TotalResults = GetNumberWithinDays(-30);
          }
        }
      }

      return result;
    }

    /// <summary>
    /// Returns a list of contributions which have been added within a given timeframe
    /// </summary>
    /// <param name="days">The number of days within the current date to set as the range</param>
    /// <param name="skip">The number of results to skip, which is used by the paging infrastructure</param>
    private IList<Contribution> GetWithinDays(int days, int skip)
    {
      return ModeratedContributions
               .Where(c => c.AddedOn >= DateTime.Now.AddDays(days))
               .OrderByDescending(c => c.AddedOn)
               .Skip(skip)
               .Take(Constants.PageSize)
               .ToList();
    }

    /// <summary>
    /// Returns the number of contributions which have been added within a given timeframe
    /// </summary>
    /// <param name="days">The number of days within the current date to set as the range</param>
    private int GetNumberWithinDays(int days)
    {
      return ModeratedContributions
         .Where(c => c.AddedOn >= DateTime.Now.AddDays(days))
         .Count();
    }

    /// <summary>
    /// Returns a set of the currently unmoderated Contributions
    /// </summary>
    private static IQueryable<Contribution> UnmoderatedContributions
    {
      get { return Repository<Contribution>.Find(c => c.ApprovedById == null); }
    }

    /// <summary>
    /// Returns a set of the currently moderated Contributions
    /// </summary>
    private IQueryable<Contribution> ModeratedContributions
    {
      get 
      {
        int? contributorId = (CurrentMember == null) ? null : (int?)CurrentMember.Id;
        return Repository<Contribution>.Find(c => c.ApprovedById != null || c.ContributorId == contributorId); 
      }
    }

    /// <summary>
    /// Gets the popular tags.
    /// </summary>
    public IList<Tag> GetPopularTags()
    {      
      using (UnitOfWork.Begin())
      {
        IQueryable<Tag> tags = Repository<Tag>.Find();
        IQueryable<ContributionTag> ctags = Repository<ContributionTag>.Find();

        var q = (from tag in tags
                join ctag in ctags on tag.Id equals ctag.TagId
                join contribution in ModeratedContributions on ctag.ContributionId equals contribution.Id
        select tag);

        IList<Tag> results = q.OrderByDescending(t => t.Usages).ToList();

        var x = (from tag in results select tag).Distinct();

        return x.Take(Constants.PageSize).ToList();
      }
    }

    /// <summary>
    /// Gets all tags.
    /// </summary>
    public IList<Tag> GetAllTags()
    {
      using (UnitOfWork.Begin())
      {
        IQueryable<Tag> tags = Repository<Tag>.Find();
        IQueryable<ContributionTag> ctags = Repository<ContributionTag>.Find();

        var q = (from tag in tags
                join ctag in ctags on tag.Id equals ctag.TagId
                join contribution in ModeratedContributions on ctag.ContributionId equals contribution.Id
        select tag).Distinct();

        return q.ToList();
      }
    }

    /// <summary>
    /// Approves an unmoderated <see cref="Contribution"/>.
    /// </summary>
    /// <param name="id">The unique identifier of the Contribution.</param>
    public void ApproveContribution(int id)
    {
      Contribution contribution = GetContribution(id);

      using (UnitOfWork.Begin())
      {
        contribution = UnmoderatedContributions
          .Where(c => c.Id == id)
          .SingleOrDefault();

        if (contribution != null)
        {
          contribution.ApprovedBy = CurrentMember;
          Repository<Contribution>.Save(contribution);
          UnitOfWork.Complete();
        }
      }
    }

    /// <summary>
    /// Deletes an existing <see cref="Contribution"/>.
    /// </summary>
    /// <param name="id">The unique identifier of the Contribution.</param>
    public void DeleteContribution(int id)
    {
      Contribution contribution = GetContribution(id);

      if (contribution != null)
      {
        using (UnitOfWork.Begin())
        {
          Repository<Contribution>.Remove(contribution);
          UnitOfWork.Complete();
        }
      }

      RedirectTo("~/Default.aspx");
    }

    private Contribution GetContribution(int id, bool bumpViews)
    {
      Contribution contribution;

      using (UnitOfWork.Begin())
      {
        contribution = ModeratedContributions
          .Where(c => c.Id == id)
          .SingleOrDefault();

        if ((contribution != null) && bumpViews)
        {
          contribution.Views++;

          UnitOfWork.Complete();
        }

        if (contribution == null && CurrentMember.IsModerator)
        {
          contribution = UnmoderatedContributions
          .Where(c => c.Id == id)
          .SingleOrDefault();

          if (bumpViews)
          {
            contribution.Views++;
            UnitOfWork.Complete();
          }
        }
      }

      return contribution;
    }

    /// <summary>
    /// Adds a new <see cref="Contribution"/>.
    /// </summary>
    /// <remarks>Security is being applied to prevent unauthenticated users making this call</remarks>
    [PrincipalPermission(SecurityAction.Demand, Authenticated = true)]
    public void AddContribution(Contribution contribution)
    {
      Invariant.ArgumentNotNull(contribution, "contribution");

      using (UnitOfWork.Begin())
      {
        contribution.Contributor = CurrentMember;

        Repository<Contribution>.Add(contribution);

        UnitOfWork.Complete();
      }

      RedirectTo("~/View.aspx", "id=" + contribution.Id);
    }

    /// <summary>
    /// Updates an existing <see cref="Contribution"/>.
    /// </summary>
    /// <remarks>Security is being applied to prevent unauthenticated users making this call</remarks>
    [PrincipalPermission(SecurityAction.Demand, Authenticated = true)]
    public void UpdateContribution(Contribution contribution)
    {
      Invariant.ArgumentNotNull(contribution, "contribution");

      using (UnitOfWork.Begin())
      {
        Repository<Contribution>.Save(contribution);

        UnitOfWork.Complete();
      }
    }

    /// <summary>
    /// Gets an RSS feed associated with the current application view
    /// </summary>
    /// <param name="channel">The text in the channel element for the RSS feed.</param>
    /// <param name="description">The text in the description element for the RSS feed.</param>
    public XmlDocument GetRssFeed(string channel, string description)
    {
      GenericRssChannel rssFeed = new GenericRssChannel();
      rssFeed["title"] = channel;
      rssFeed["link"] = "http://www.backgroundmotion.com";
      rssFeed["description"] = description;
      rssFeed["copyright"] = "(c) 2007 - Mindscape";
      rssFeed["lastBuildDate"] = DateTime.Now.ToString("ddd, dd MMM yyyy HH:mm:ss");
      rssFeed["generator"] = "Background Motion RSS";
      rssFeed["managingEditor"] = "webmaster@backgroundmotion.com";
      rssFeed["webMaster"] = "webmaster@backgroundmotion.com";

      foreach (Contribution contribution in Search(1).Contributions)
      {
        GenericRssElement item = new GenericRssElement();
        item["title"] = contribution.Title;
        item["description"] = contribution.Description;
        item["pubDate"] = contribution.AddedOn.ToString("ddd, dd MMM yyyy HH:mm:ss");
        item["category"] = contribution.ContentType.Description;
        item["guid"] = contribution.Id.ToString();

        if (!string.IsNullOrEmpty(contribution.PreviewImage))
        {
          GenericRssEnclosure enclosure = new GenericRssEnclosure();
          enclosure["url"] = contribution.PreviewImage;
          enclosure["type"] = "image/jpeg";
          item.Enclosure = enclosure;
        }

        rssFeed.Items.Add(item);
      }

      return rssFeed.SaveAsXml();
    }
  }
}